#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/tty.h>

#include "log.h"
#include "scull_common.h"

#define SCULL_NAME "scullpriv"

char *scull_name = SCULL_NAME;
spinlock_t scull_c_lock = __SPIN_LOCK_UNLOCKED(scull_c_lock);
LIST_HEAD(scull_dev_list);
struct scull_dev *scull_rdev = NULL;

struct scull_listitem {
    struct scull_dev *sdev;
    dev_t key;
    struct list_head list;
};

static struct scull_dev *scull_c_lookup_dev(dev_t key) {
    struct scull_listitem *p;
    unsigned long flags = 0;

    spin_lock_irqsave(&scull_c_lock, flags);
    list_for_each_entry(p, &scull_dev_list, list) {
        if (p->key == key) {
            spin_unlock_irqrestore(&scull_c_lock, flags);
            LOGI("found existing scull_dev for devno %u,%u", MAJOR(key), MINOR(key));
            return p->sdev;
        }
    }
    spin_unlock_irqrestore(&scull_c_lock, flags);

    p = kzalloc(sizeof(struct scull_listitem), GFP_KERNEL);
    if (p == NULL) {
        LOGE("alloc scull_listitem failed");
        return NULL;
    }

    p->sdev = kzalloc(sizeof(struct scull_dev), GFP_KERNEL);
    if (p->sdev == NULL) {
        LOGE("alloc scull_dev failed");
        kfree(p);
        p = NULL;
        return NULL;
    }

    p->key = key;
    INIT_LIST_HEAD(&p->list);
    scull_trim(p->sdev);
    sema_init(&p->sdev->sem, 1);

    spin_lock_irqsave(&scull_c_lock, flags);
    list_add(&p->list, &scull_dev_list);
    LOGI("create new scull_dev for devno %u,%u", MAJOR(key), MINOR(key));

    spin_unlock_irqrestore(&scull_c_lock, flags);
    return p->sdev;
}

static int scull_c_open(struct inode *inode, struct file *filp) {
    LOGI("%s enter\n", __FUNCTION__);
    struct scull_dev *sdev = NULL;
    dev_t devno;
    int append = 0;
    int res = 0;

    if (!current->signal->tty) {
        LOGE("no tty for current process");
        return -ENODEV;
    }
    devno = tty_devnum(current->signal->tty);

    sdev = scull_c_lookup_dev(devno);
    if (sdev == NULL) {
        return -ENOMEM;
    }

    sdev->quantum_size = scull_rdev->quantum_size;
    sdev->qset_size = scull_rdev->qset_size;
    append = filp->f_flags & O_APPEND;
    if ((filp->f_flags & O_ACCMODE) == O_WRONLY) {
        if (!append) {
            scull_trim(sdev);
        } else {
            filp->f_pos = sdev->length;
        }
    }
    filp->private_data = sdev;
    res = 0;

    LOGI("%s exit\n", __FUNCTION__);
    return res;
}

static int scull_c_release(struct inode *inode, struct file *filp) {
    LOGI("%s enter\n", __FUNCTION__);
    LOGI("%s exit\n", __FUNCTION__);
    return 0;
}

struct file_operations scull_fops = {
    .owner = THIS_MODULE,
    .open = scull_c_open,
    .release = scull_c_release,
    .read = scull_read,
    .write = scull_write,
    .llseek = scull_llseek,
};

static int scull_c_init(void)
{
    LOGI("%s enter\n", __FUNCTION__);
    int res = 0;

    scull_rdev = kzalloc(sizeof(struct scull_dev), GFP_KERNEL);
    if (scull_rdev == NULL) {
        LOGE("alloc scull_dev failed");
        return -ENOMEM;
    }

    res = scull_setup_cdev(scull_rdev, 0, &scull_fops);
    if (res) {
        kfree(scull_rdev);
        scull_rdev = NULL;
        return res;
    }
    LOGI("register chrdev region, major %d, minor %d, count %d",
            scull_major, scull_minor, scull_dev_nr);

    LOGI("%s exit\n", __FUNCTION__);
    return 0;
}
module_init(scull_c_init);

static void scull_c_exit(void)
{
    LOGI("%s enter\n", __FUNCTION__);
    struct scull_listitem *p, *t;
    unsigned long flags;

    spin_lock_irqsave(&scull_c_lock, flags);
    list_for_each_entry_safe(p, t, &scull_dev_list, list) {
        list_del(&p->list);

        scull_trim(p->sdev);
        kfree(p->sdev);
        p->sdev = NULL;
        kfree(p);
        p = NULL;
    }

    if (scull_rdev) {
        scull_delete_cdev(scull_rdev, 0);
        kfree(scull_rdev);
        scull_rdev = NULL;
    }
    spin_unlock_irqrestore(&scull_c_lock, flags);
    LOGI("%s exit\n", __FUNCTION__);
}
module_exit(scull_c_exit);

MODULE_LICENSE("GPL");